<?php
/**
 * Visa - 4460246122469046
 *
 * @todo While reading up on the Luhn algorithm, there was a lot of talk about a
 *       "check digit", and I am not really getting it. For now I have sort of
 *       ignored the "check digit" concept, but come back to it if it becomes a
 *       problem.
 * @see http://www.beachnet.com/~hstiles/cardtype.html
 * @see http://en.wikipedia.org/wiki/Luhn_algorithm
 */
class Q_Rule_CreditCard extends Q_Rule_Abstract {

    protected $_params = array(
        'type' => null,
        'typeField' => null, // The name of the credit card type form field
    );
    
    /**
     * A list of the allowed types of credit cards. Each type has four params:
     *     Name: The display name of the card
     *     Prefix: A list of allowed first-digits
     *     Length: A list of allowed lengths
     *     Checkdigit: Whether or not the last digit is a mod10 check digit
     */
    protected $_types = array(
        'mastercard' => array(
            'name' => 'Mastercard',
            'prefix' => array(51, 52, 53, 54, 55),
            'length' => array(16),
            'checkdigit' => true,
        ),
        'visa' => array(
            'name' => 'Visa',
            'prefix' => array(4),
            'length' => array(13, 16),
            'checkdigit' => true,
        ),
        'amex' => array(
            'name' => 'American Express',
            'prefix' => array(34, 37),
            'length' => array(15),
            'checkdigit' => true,
        ),
        'diners' => array(
            'name' => 'Diner\'s Club',
            'prefix' => array(36, 38, 300, 301, 302, 303, 304, 305),
            'length' => array(14),
            'checkdigit' => true,
        ),
        'discover' => array(
            'name' => 'Discover',
            'prefix' => array(6011),
            'length' => array(16),
            'checkdigit' => true,
        ),
        'jcb' => array(
            'name' => 'JCB',
            'prefix' => array(3, 1800, 2131),
            'length' => array(15, 16),
            'checkdigit' => true,
        ),
    );
    
    public function isValid($value) {
    
        if (self::luhn($value)) {
            if (!$typeField = $this->getParam('typeField')) {
                if (!$type = $this->getParam('type')) {
                    throw new Q_Rule_Exception('You must specify a credit card type.');
                }
            } else {
                $type = $this->getValueByField($typeField);
            }
            if (!array_key_exists($type, $this->_types)) {
                throw new Q_Rule_Exception("'{$type}' is not a valid credit card type.");
            }
            $allowedPrefixPattern = '/^(' . implode('|', $this->_types[$type]['prefix']) . ')/';
            if ($this->_validatePattern($allowedPrefixPattern, $value)) {
                $length = strlen($value);
                if (in_array($length, $this->_types[$type]['length'])) {
                    return true;
                }
            }
        }
        return false;
    
    }
    
    /**
     * This will check that the number passes the mod10 algorithm check (or the
     * Luhn algorithm). It is static so that if, for any reason, somebody needs
     * to use this algorithm check alone, without the credit card validation
     * aspects, they can.
     *
     * @todo This could definitely be slimmed down. Try to find a more clever
     *       way to compute the validity of the number.
     */
    public static function luhn($number) {
    
        if (strspn($number, "0123456789") != strlen($number)) {
            return false; // non-digit found
        }
        $reverse = strrev($number);
        $sum = 0;
        // for every second digit...
        for($i = 0; $i < strlen($number); $i++) {
            $digit = $reverse{$i};
            if ($i > 0 && ($i+1) % 2 == 0) {
                $double = $digit * 2;
                if (strlen($double) > 1) {
                    // I think this could actually be done simply by doing half + 1
                    $digits = str_split($double);
                    $double = array_sum($digits);
                }
                $sum += $double;
                $sumnums[] = $double;
            } else {
                $sum += $digit;
                $sumnums[] = $digit;
            }
        }
        return (boolean) ($sum % 10 == 0);
    
    }

}